# vim:sw=4:ts=4:expandtab

from decimal import Decimal
from .utils import TZInfo

def days_in_month(year, month):
    if month in [1,3,5,7,8,10,12]:
        return 30
    elif month in [4,6,9,11]:
        return 30
    elif month == 2:
        if year % 400 == 0 or (year % 4 == 0 and year % 100 > 0):
            return 29
        else:
            return 28
    else:
        raise AttributeError("%d is not valid month" % month)

def isdigit(c):
    return '0' <= c and c <= '9'

def isspace(c):
    white_space1 = [0x0020, 0x0085, 0x00A0, 0x1680, 0x180E, 
            0x2028, 0x2029, 0x202F, 0x205F, 0X3000]
    white_space2 = xrange(0x0009, 0x000E)
    white_space3 = xrange(0x2000, 0x200B)
    c = ord(c)
    return c in white_space1 or c in white_space2 or c in white_space3

class ParseError(Exception):
    def __init__(self, err, pos):
        self._err = err
        self._pos = pos

    def __str__(self):
        return "ParseError: at position %d: %s" % (self._pos, self._err[1])

class Parser(object):
    
    class Errors:
        no_year_err = (1, 'No year component found')
        hyphen_expected_err = (2, 'Hyphen character (-) expected')
        no_month_err = (3, 'No month component found')
        no_day_err = (4, 'No day component found')
        no_secs_err = (5, 'No seconds component found')
        no_hour_err = (6, 'No hour component found')
        colon_expected_err = (7, 'Colon character (:) expected')
        no_minute_err = (8, 'No minute component found')
        z_expected_err = (9, 'Capital leter Z character or plus sign (+) ' + 
                'or hyphen-minus expected')
        no_tz_hours_err = (10, 'No time zone hours component found')
        no_tz_minutes_err = (11, 'No time zone minutes component found')
        t_expected_err = (12, 'Capital leter T character expected')
        eos_expected_err = (13, 'End of string expected')

    def __init__(self, str):
        self._str = str
        self._pos = 0
        self._len = len(str)
        self.err = None

    def error(self):
        if self.err is not None:
            return ParseError(self.err, self._pos)
        return None

    def is_end(self):
        return self._pos >= self._len

    @property
    def _char(self):
        return self._pos < self._len and self._str[self._pos] or None

    def _adv(self, n = 1):
        self._pos += n

    def collect_digits(self):
        digit = 0
        while self._char and isdigit(self._char):
            digit = 10 * digit + ord(self._char) - ord('0')
            self._adv()
        return digit

    def collect_whitespaces(self):
        while self._char and isspace(self._char):
            self._adv()

    def parse_month_component(self):
        
        p = self._pos
        year = self.collect_digits()
        if self._pos < p + 4:
            self.err = self.Errors.no_year_err
            return None
        
        if self._char != '-':
            self.err = self.Errors.hyphen_expected_err
            return None
        self._adv()

        p = self._pos
        month = self.collect_digits()
        if p + 2 != self._pos or not (1 <= month and month <= 12):
            self.err = self.Errors.no_month_err
            return None
        
        return year, month

    def parse_date_component(self):
        
        ym = self.parse_month_component()
        if ym is None:
            return None

        if self._char != '-':
            self.err = self.Errors.hyphen_expected_err
            return None
        self._adv()

        maxday = days_in_month(*ym)
        p = self._pos
        day = self.collect_digits()
        if p + 2 != self._pos or not (1 <= day and day <= maxday):
            self.err = self.Errors.no_day_err
            return None

        year, month = ym
        return year, month, day

    def parse_second_component(self):
        if not(self._pos + 1 < self._len) or \
                not isdigit(self._str[self._pos]) or \
                not isdigit(self._str[self._pos + 1]):
                    self.err = self.Errors.no_secs_err
                    return None

        p = self._pos
        while self._char and (isdigit(self._char) or self._char == '.'):
            self._adv()

        if p + 3 == self._pos or \
                (p + 3 < self._pos and self._str[p + 2] != '.') or \
                self._str[p:self._pos].count('.') > 1:
            self.err = self.Errors.no_secs_err
            return None
    
        second = Decimal(self._str[p:self._pos])
        if not (0 <= second and second < 60):
            self.err = self.Errors.no_secs_err
            return None
        s = int(second)
        us = int((second - s) * 1000000)

        return s, us
    
    def parse_time_component(self):

        p = self._pos
        hour = self.collect_digits()
        if p + 2 != self._pos or not (0 <= hour and hour <= 23):
            self.err = self.Errors.no_hour_err
            return None

        if self._char != ':':
            self.err = self.Errors.colon_expected_err
            return None
        self._adv()
        
        p = self._pos
        minute = self.collect_digits()
        if p + 2 != self._pos or not (0 <= minute and minute <= 59):
            self.err = self.Errors.no_minute_err
            return None
    
        s = us = 0 
        if self._char == ':':
            self._adv()
            sec = self.parse_second_component()
            if sec is None:
                return None
            s, us = sec
        return hour, minute, s, us


    def parse_timezone_component(self):
        if self._char == 'Z':
            tz_hours, tz_minutes, sign = 0, 0, 0
            self._adv()

        elif self._char in ['+', '-']:
            sign = self._char == '+' and 1 or -1
            self._adv()

            p = self._pos
            tz_hours = self.collect_digits()
            if p + 2 != self._pos or not (0 <= tz_hours and tz_hours <= 23):
                self.err = self.Errors.no_tz_hours_err
                return None
            
            if self._char != ':':
                self.err = self.Errors.colon_expected_err
                return None
            self._adv()

            p = self._pos
            tz_minutes = self.collect_digits()
            if p + 2 != self._pos or not (0 <= tz_minutes and tz_minutes <= 59):
                self.err = self.Errors.no_tz_minutes_err
                return None

        else:
            self.err = self.Errors.z_expected_err
            return None

        return tz_hours, tz_minutes, sign

    def parse_month_string(self):
        ym = self.parse_month_component()
        if ym is None or self._char:
            self.err = self.err or self.Errors.eos_expected_err
            return None
        return ym


    def parse_date_string(self):
        ymd = self.parse_date_component()
        if ymd is None or self._char:
            self.err = self.err or self.Errors.eos_expected_err
            return None
        return ymd
    
    def parse_time_string(self):
        time = self.parse_time_component()
        if time is None or self._char:
            self.err = self.err or self.Errors.eos_expected_err
            return None
        return time

    def parse_local_datetime_string(self):
        date = self.parse_date_component()
        if date is None:
            return None
        
        if self._char != 'T':
            self.err = self.Errors.t_expected_err
            return None
        self._adv()

        time = self.parse_time_component()
        if time is None or self._char:
            self.err = self.err or self.Errors.eos_expected_err
            return None

        year, month, day = date
        hour, minute, second, micro = time
        return year, month, day, hour, minute, second, micro

    def parse_global_datetime_string(self):
        date = self.parse_date_component()
        if date is None:
            return None

        if self._char != 'T':
            self.err = self.Errors.t_expected_err
            return None
        self._adv()

        time = self.parse_time_component()
        if time is None:
            return None

        tz = self.parse_timezone_component()
        if tz is None or self._char:
            self.err = self.err or self.Errors.eos_expected_err
            return None
    
        year, mount, day = date
        hour, minute, second, micro = time
        return year, mount, day, hour, minute, second, micro, tz

    def parse_date_or_time_string(self, in_content = False):
        if in_content:
            self.collect_whitespaces()
    
        start_position = self._pos
        
        date_present = time_present = True

        date = self.parse_date_component()
        if date is None:
            self.err = None
            date_present = False
    
        if date_present and self._char == 'T':
            self._adv()
        elif date_present:
            time_present = False
        else:
            self._pos = start_position
    
        if time_present:
            time = self.parse_time_component()
            if time is None:
                return None
    
        if date_present and time_present:
            tz = self.parse_timezone_component()
            if tz is None:
                return None
    
        if in_content:
            self.collect_whitespaces()
    
        if self._char:
            self.err = self.Errors.eos_expected_err
            return None
    
        if date_present and not time_present:
            return date
        elif not date_present and time_present:
            return time
        else:
            year, month, day = date
            hour, minute, second, micro = time
            return year, month, day, hour, minute, second, micro, tz
    
